package defectsim;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import repast.simphony.context.Context;
import repast.simphony.engine.environment.RunEnvironment;
import repast.simphony.random.RandomHelper;
import repast.simphony.space.continuous.ContinuousSpace;
import repast.simphony.space.continuous.NdPoint;
import repast.simphony.space.grid.Grid;
import repast.simphony.space.grid.GridPoint;
import repast.simphony.valueLayer.GridValueLayer;
import datamanager.DataCollector;
import datamanager.DataWriter;
import defectsim.DefectFactory.DefectType;
import mathutils.*;

public class SimEnvironment {

	private Context<DefectAgent> context;

	// Initial configuration
	private int initialNumberOfDislocations = 1000;
	private int initialNumberOfDislocationSources = 20;
	private int initialNumberOfObstacles = 0;

	// Material properties
	private Material currentMaterial;

	// Global properties
	private double spaceLength;
	private int gridSize;
	private String spaceName = "Continuous Space";
	private String gridName = "Simple Grid";
	private double minCutOffRadius;
	private double maxCutOffRadius;
	private double initialTimeStep;
	private double timeStep;
	private Stress stress;
	private double annihilationLength;
	private double dipoleLength;
	private double cyclicTickCount;
	private boolean multiplication;
	private int preNumberOfDipole = 0;
	private boolean stable = false;

	// Data collectors
	private String elasticStressFieldName;
	private int annihilated = 0;
	private int nucleated = 0;
	private boolean saveData;

	private DataCollector dc = new DataCollector();

	public double getCurrentExternalStress(){
		return stress.getCurrentExternalStress(this.getCurrentTick());
	}

	public double getCurrentFrictionStress(){
		return stress.getCurrentFrictionStress(this.getCurrentTick());
	}

	public void setStress(Stress stress) {
		this.stress = stress;
	}

	public Material getCurrentMaterial() {
		return currentMaterial;
	}

	public void setCurrentMaterial(MaterialFactory.MaterialName mName) {
		this.currentMaterial = MaterialFactory.createMaterial(mName);
	}

	public String getElasticStressFieldName() {
		return elasticStressFieldName;
	}

	public void setElasticStressFieldName(String elasticStressFieldName) {
		this.elasticStressFieldName = elasticStressFieldName;
	}

	public double getMinCutOffRadius() {
		return minCutOffRadius;
	}

	public void setMinCutOffRadius(double minCutOffRadius) {
		this.minCutOffRadius = minCutOffRadius;
	}

	public double getMaxCutOffRadius() {
		return maxCutOffRadius;
	}

	public void setMaxCutOffRadius(double maxCutOffRadius) {
		this.maxCutOffRadius = maxCutOffRadius;
	}

	public double getInitialTimeStep() {
		return initialTimeStep;
	}

	public void setInitialTimeStep(double initialTimeStep) {
		this.initialTimeStep = initialTimeStep;
		this.timeStep = initialTimeStep;
	}

	public double getTimeStep() {
		return timeStep;
	}

	public void setTimeStep(double timeStep) {
		this.timeStep = timeStep;
	}

	public double getSpaceLength() {
		return spaceLength;
	}

	public void setSpaceLength(double spaceLength) {
		this.spaceLength = spaceLength;
	}

	public int getGridSize() {
		return gridSize;
	}

	public void setGridSize(int gridSize) {
		this.gridSize = gridSize;
	}

	public String getSpaceName() {
		return spaceName;
	}

	public void setSpaceName(String spaceName) {
		this.spaceName = spaceName;
	}

	public String getGridName() {
		return gridName;
	}

	public void setGridName(String gridName) {
		this.gridName = gridName;
	}

	public Context<DefectAgent> getContext() {
		if (context == null) System.out.println("Context is null.");
		return context;
	}

	public void setContext(Context<DefectAgent> context) {
		this.context = context;
	}

	public double getAnnihilationLength() {
		return annihilationLength * currentMaterial.getBurgersVector();
	}

	/**
	 * @param annihilationLength is multiplier for Burgers vector of material
	 */
	public void setAnnihilationLength(double annihilationLength) {
		this.annihilationLength = annihilationLength;
	}

	public double getDipoleLength() {
		return dipoleLength * currentMaterial.getBurgersVector();
	}

	public void setDipoleLength(double dipoleLength) {
		this.dipoleLength = dipoleLength;
	}

	public double getCyclicTickCount() {
		return cyclicTickCount;
	}

	public void setCyclicTickCount(double cyclicTickCount) {
		this.cyclicTickCount = cyclicTickCount;
	}

	public double getCycleNumber(){
		return this.getCurrentTick() / this.cyclicTickCount;
	}

	public double getCurrentTick(){
		return RunEnvironment.getInstance().getCurrentSchedule().getTickCount();
	}

	public boolean isSaveData() {
		return saveData;
	}

	public void setSaveData(boolean saveData) {
		this.saveData = saveData;
	}

	public int getInitialNumberOfDislocations() {
		return initialNumberOfDislocations;
	}

	public void setInitialNumberOfDislocations(int initialNumberOfDislocations) {
		this.initialNumberOfDislocations = initialNumberOfDislocations;
	}

	public int getInitialNumberOfDislocationSources() {
		return initialNumberOfDislocationSources;
	}

	public void setInitialNumberOfDislocationSources(
			int initialNumberOfDislocationSources) {
		this.initialNumberOfDislocationSources = initialNumberOfDislocationSources;
	}

	public int getInitialNumberOfObstacles() {
		return initialNumberOfObstacles;
	}

	public void setInitialNumberOfObstacles(int initialNumberOfObstacles) {
		this.initialNumberOfObstacles = initialNumberOfObstacles;
	}

	@SuppressWarnings("unchecked")
	public NdPoint getLocation(DefectAgent da){
		ContinuousSpace space = (ContinuousSpace) context.getProjection(spaceName);
		return space.getLocation(da);
	}


	public boolean isMultiplication() {
		return multiplication;
	}

	public void setMultiplication(boolean multiplication) {
		this.multiplication = multiplication;
	}
	
	public int getAnnihilated(){
		return annihilated;
	}
	
	public int getNucleated(){
		return nucleated;
	}

	@SuppressWarnings("unchecked")
	public List<Dislocation> getNeighbours(Dislocation agent, double perceptionRadius){
		List<Dislocation> neighbours = new ArrayList<Dislocation>();
		ContinuousSpace space = (ContinuousSpace) context.getProjection(spaceName);
		NdPoint p1 = space.getLocation(agent);
		Iterable<Dislocation> allNeighbours = space.getObjects();
		for (Object dObj : allNeighbours) {
			if (dObj instanceof Dislocation){
				Dislocation dI = (Dislocation)dObj;
				NdPoint p2 = space.getLocation(dI);
				double dist = Util.calculateMICDistance(p1, p2, this.spaceLength);
//				double dist = Util.calculateDistance(p1, p2);
				if ((dist != 0.0) && (dist <= perceptionRadius))
					neighbours.add(dI);
			}
		}
		return neighbours;
	}

	@SuppressWarnings("unchecked")
	public List<Dislocation> getAllNeighbours(Dislocation agent){
		List<Dislocation> neighbours = new ArrayList<Dislocation>();
		ContinuousSpace space = (ContinuousSpace) context.getProjection(spaceName);
		Iterable<Dislocation> allNeighbours = space.getObjects();
		for (Object dObj : allNeighbours) {
			if (dObj instanceof Dislocation){
				Dislocation dI = (Dislocation)dObj;
				if (dI != agent) 
					neighbours.add(dI);
			}
		}
		return neighbours;
	}

	@SuppressWarnings("unchecked")
	public void moveByVector(DefectAgent da, double distance, double direction){
		double gridScale =  gridSize / spaceLength;
		ContinuousSpace space = (ContinuousSpace) context.getProjection(spaceName);
		Grid grid = (Grid)context.getProjection(gridName);
		double dx = distance * Math.cos(direction);
		double dy = distance * Math.sin(direction);
		space.moveByDisplacement(da, dx, dy, 0);
		NdPoint point = space.getLocation(da);
		grid.moveTo(da, (int)(point.getX() * gridScale), (int)(point.getY() * gridScale));
	}

	public void annihilate(DefectAgent da){
		context.remove(da);
		annihilated++;
	}

	public void nucleate(DefectAgent da){
		context.add(da);
		nucleated++;
	}
	
	public void updatePerception(DefectAgent da){
		
	}

	@SuppressWarnings("unchecked")
	public void update(){
		int numberOfMobile = 0;
		int numberOfDislocation = 0;
		int numberOfDipole = 0;

		double totalDistance = 0;
		double totalVelocity = 0;
		double strainRate = 0;
		double maxVelocity = 0;
		double maxDistance = 0;
		double totalStress = 0;
		double totalEnergy = 0;
		
		if (saveData) DefectDomain.zeroDomains();

		ContinuousSpace space = (ContinuousSpace) context.getProjection(spaceName);
		Iterable<Dislocation> allNeighbours = space.getObjects();
		for (Object dObj : allNeighbours) {
			if (dObj instanceof Dislocation){
				Dislocation d = (Dislocation)dObj;
				if(d.isMobile()){
					numberOfMobile++;
					double absVelocity = Math.abs(d.getVelocity());
					double absDistance = Math.abs(d.getDistance());
					totalDistance += d.getDistance();
					totalVelocity += absVelocity;
					totalStress += d.getStress();
					totalEnergy += absVelocity * absVelocity;
					if (absVelocity > maxVelocity) maxVelocity = absVelocity;
					if (absDistance > maxDistance) maxDistance = absDistance;
					//					System.out.println(d.getVelocity());
					strainRate += d.getUnitBurgersVector() * currentMaterial.getBurgersVector() * d.getVelocity();
					if (saveData) DefectDomain.updateDefectDomain(d);
				}
				if(d.isDipole())
					numberOfDipole++;
				numberOfDislocation++;
			}
		}
		double meanVelocity = totalVelocity / numberOfDislocation;
		double meanDistance = totalDistance / numberOfDislocation;
		double meanStress = totalStress / numberOfDislocation;
		// Orowan stress criterion Eq. 27 from "Dislocation Dynamics I. A Proposed...", R.J. Amodeo & N.M.Ghoniem, 1990
		double orowanStress = Math.abs(2 * currentMaterial.getShearModulus() * currentMaterial.getBurgersVector() / currentMaterial.getInterobstacleSpacing());
		int percent = (int)(100 * numberOfMobile / numberOfDislocation);
		double density = numberOfDislocation / (this.spaceLength * this.spaceLength);
		double mobileDensity = numberOfMobile / (this.spaceLength * this.spaceLength);
		double strain = currentMaterial.getBurgersVector() * 1e-3 * mobileDensity * meanDistance;
		strainRate *= mobileDensity * 1e3;
		//		double strainRateGlobal = density * 1e-6 * currentMaterial.getBurgersVector() * meanVelocity;
		System.out.println("# of Dislocation: " + numberOfDislocation + 
				", # Mobile: " + numberOfMobile +
				", % " + percent + 
				", Annihilated: " + annihilated + 
				", Nucleated: " + nucleated +
				", Dipole: " + numberOfDipole);
		System.out.println("Cycle: " + this.getCycleNumber());
		//		double strain = currentMaterial.getBurgersVector() * totalDistance;
		System.out.println("Mean Velocity: " + meanVelocity + " m/s");
		System.out.println("Max Velocity: " + maxVelocity + " m/s");
		System.out.println("Mean Distance: " + meanDistance * 1e3 + " nm");
		System.out.println("Max Distance: " + maxDistance * 1e3 + " nm");
		System.out.println("Energy: " + totalEnergy + " ??");
		//		System.out.println("Mean Stress: " + meanStress + " GPa");
		//		System.out.println("Orowan Stress: " + orowanStress + " GPa");
		//		System.out.println("Stress Diff: " + (Math.abs(meanStress) - orowanStress));
		//		System.out.println("Strain: " + strain / this.timeStep);
		//		System.out.println("Strain Rate: " + strainRate);
		////		System.out.println("Strain Rate (Global): " + strainRateGlobal / this.timeStep);
		//		System.out.println("Dislocation Density: " + density + " μm-2");
		//		double densityRate = density * meanVelocity / currentMaterial.getInterobstacleSpacing();
		//		System.out.println("Dislocation Density Rate: " + densityRate + " μm-2s-1");
		//		System.out.println("# of new dislocations: " + this.timeStep * meanVelocity / currentMaterial.getInterobstacleSpacing());
		//		System.out.println("# of new dislocations: " + this.timeStep * K1 * strainRateGlobal);
		dc.setAnnihilated(annihilated);
		dc.setDipole(numberOfDipole);
		dc.setMobile(numberOfMobile);
		dc.setTotal(numberOfDislocation);
		//		calculateFFT();

		// Limit time step if velocity is too large
//		double maxD = 0.1; // 50 nanometer
//		double dt = maxD * 1e-6 / maxVelocity;
//		if(dt != Double.POSITIVE_INFINITY) this.timeStep = dt;
//		System.out.println("Time Step: " + this.timeStep + " s");

		// Stop simulation if a stable pattern is formed
		// Check the change in number of dipoles
		double tic = getCurrentTick();
		if (Math.IEEEremainder(tic, this.cyclicTickCount) == 0.0){
			if (numberOfDipole > 0 && preNumberOfDipole > 0){
				double diff = (double)Math.abs(numberOfDipole - preNumberOfDipole) / preNumberOfDipole;
				System.out.println(numberOfDipole + " " + preNumberOfDipole + " " + diff);
//				if (diff < 0.01) RunEnvironment.getInstance().endRun();
			}
			preNumberOfDipole = numberOfDipole;
		}
		
		// Check whether a stable spatial distribution is achieved
//		if (tic > 5 && meanVelocity == 0.0 && !stable){
//			Stress stress = new Stress(0.01, 0.00036, Stress.ExternalStressType.Cyclic, this.getCyclicTickCount());
//			this.stress = stress;
//			stable = true;
//		}
		
		// Dislocation multiplication 
		if (Math.IEEEremainder(tic, this.cyclicTickCount/2) == 0.0){
//		if(multiplication){
			System.out.println("Multiplying...");
			multiplication = false;
			int numDislocations = 4;
			double gridScale = getGridSize() / getSpaceLength();
			double[] slipsys = getCurrentMaterial().getSlipSystems();
			Material material = getCurrentMaterial();
			int noDislocationForEachSystem = (int) numDislocations / 
			(2 * material.getNumberOfActiveSlipSystems());
			for (int s = 0; s < material.getNumberOfActiveSlipSystems(); s++){

				for (int i = 0; i < noDislocationForEachSystem; i++) {
					EdgeDislocation dislocation = (EdgeDislocation) DefectFactory.createDefect(DefectType.EdgeDislocation);
					dislocation.setDirection(slipsys[s]);
					dislocation.setUnitBurgersVector(1);
					dislocation.setX(findRandomLocation());
					dislocation.setY(findRandomLocation());
					dislocation.setLocationGridX((int)(dislocation.getX() * gridScale));
					dislocation.setLocationGridY((int)(dislocation.getY() * gridScale));
					context.add(dislocation);
					nucleated++;
				}
				for (int i = 0; i < noDislocationForEachSystem; i++) {
					EdgeDislocation dislocation = (EdgeDislocation) DefectFactory.createDefect(DefectType.EdgeDislocation);
					dislocation.setDirection(slipsys[s]);
					dislocation.setUnitBurgersVector(-1);
					dislocation.setX(findRandomLocation());
					dislocation.setY(findRandomLocation());
					dislocation.setLocationGridX((int)(dislocation.getX() * gridScale));
					dislocation.setLocationGridY((int)(dislocation.getY() * gridScale));
					context.add(dislocation);
					nucleated++;
				}
			}
		}
	}

	public void calculateFFT(){
		System.out.println("Entering FFT...");
		double[][] xy = new double[2 * getGridSize()][2 * getGridSize()];

		Grid grid = (Grid) this.getContext().getProjection(this.getGridName());
		Iterable<DefectAgent> allDefects = grid.getObjects();
		GridValueLayer FFTField = (GridValueLayer)this.getContext().getValueLayer("FFT Field");

		for (Object defect : allDefects) {
			try {
				if (Class.forName(EdgeDislocation.class.getName()).isInstance(defect)){
					EdgeDislocation d = (EdgeDislocation)defect;
					GridPoint location = grid.getLocation(d);
					if (!d.isDipole())
						FFTField.set(1, location.getX(),location.getY());
				}
			} catch (ClassNotFoundException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		for (int i = 0; i < getGridSize(); i++) 
			for (int j = 0; j < getGridSize(); j++) {
				xy[i][j] = FFTField.get(i, j);
			}
		DoubleFFT_2D fft = new DoubleFFT_2D(getGridSize(), getGridSize());
		fft.realForwardFull(xy);
		for (int i = 0; i < getGridSize(); i++) 
			for (int j = 0; j < getGridSize(); j++) {
				FFTField.set(xy[i][j], i, j);
			}
		System.out.println("Leaving FFT...");
	}

	public void saveData() throws IOException{
		if (saveData){
			String userDir = System.getProperty("user.dir",".");
			String fname = userDir + "/misc/data/" + "Run" + Integer.toString((int)this.getCurrentTick()) + "_" + Util.now();
			GridValueLayer elasticField = (GridValueLayer)context.getValueLayer("Elastic Field");
			GridValueLayer energyField = (GridValueLayer)context.getValueLayer("Energy Field");
			DataWriter dw = new DataWriter();
			//				dw.saveExistence(fname  + ".dat", DefectType.EdgeDislocation);
			//				//				dc.saveData(fname + ".ini");
			//				dw.saveStress(fname + ".str");
			dw.SaveValueLayer(energyField, fname + ".energy");
			dw.SaveValueLayer(elasticField, fname + ".stress");
		}
	}

	private double findRandomLocation() {
		return RandomHelper.nextDoubleFromTo(0,	getSpaceLength());
	}

	// Simulated environment class is singleton
	private SimEnvironment(){

	}

	private static SimEnvironment _instance = null;

	static public SimEnvironment getInstance() {
		if(_instance == null) {
			_instance = new SimEnvironment();
		}
		return _instance;
	}

}
